using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;

namespace Comms {
    public class Comm {
        public class Connection {
            public Guid OurGuid = Guid.NewGuid();

            public Guid TheirGuid;

            public bool InitAckReceived;

            public bool InitAckConfirmed;

            public Dictionary<uint, UnackedPacket> UnackedPackets = new();

            // ReSharper disable MemberHidesStaticFromOuterClass
            public Dictionary<uint, MessageParts> MessageParts = new();
            // ReSharper restore MemberHidesStaticFromOuterClass

            public Dictionary<uint, byte[]> SequencedBytes = new();

            public List<uint> PacketIdsToAck = new();

            public HashSet<uint> ReceivedPacketIdsOld = new();

            public HashSet<uint> ReceivedPacketIdsCurrent = new();

            public double LastReceivedPacketsIdsSwitchTime;

            public uint? NextUnreliableReceiveSequenceIndex;

            public uint NextUnreliableSendSequenceIndex;

            public uint? NextReliableReceiveSequenceIndex;

            public uint NextReliableSendSequenceIndex;

            public double LastInitAckSendTime = double.MinValue;

            public double LastSendTime = double.MinValue;

            public double LastReceiveTime = double.MinValue;

            public void NewTheirGuid(Guid theirGuid) {
                TheirGuid = theirGuid;
                MessageParts.Clear();
                SequencedBytes.Clear();
                PacketIdsToAck.Clear();
                ReceivedPacketIdsOld.Clear();
                ReceivedPacketIdsCurrent.Clear();
                NextUnreliableReceiveSequenceIndex = 0u;
                NextReliableReceiveSequenceIndex = 0u;
            }
        }

        public class MessageParts {
            public double LastReceiveTime = double.MinValue;

            public int LastPartIndex = -1;

            public Dictionary<int, byte[]> Parts = new();
        }

        public enum PacketType : byte {
            RawData = 1,
            UnreliableData,
            ReliableData,
            DataAck,
            InitAck
        }

        public struct PacketHeader {
            public bool IsInvalid;

            public PacketType PacketType;

            public bool IsConnectionInit;

            public uint PacketId;

            public Guid InitGuid;

            public static PacketHeader Read(Reader reader) {
                PacketHeader result = default;
                if (reader.Length - reader.Position >= 1) {
                    byte b = reader.ReadByte();
                    result.PacketType = (PacketType)(b & 0xF);
                    if (result.PacketType == PacketType.RawData) {
                        return result;
                    }
                    if (result.PacketType == PacketType.UnreliableData
                        || result.PacketType == PacketType.ReliableData) {
                        result.IsConnectionInit = (b & 0x80) != 0;
                        int num = result.IsConnectionInit ? 20 : 4;
                        if (reader.Length - reader.Position >= num) {
                            if (result.IsConnectionInit) {
                                result.InitGuid = new Guid(reader.ReadFixedBytes(16));
                            }
                            result.PacketId = reader.ReadUInt32();
                            return result;
                        }
                    }
                    else if (result.PacketType == PacketType.DataAck) {
                        if (reader.Length - reader.Position >= 4) {
                            return result;
                        }
                    }
                    else if (result.PacketType == PacketType.InitAck
                        && reader.Length - reader.Position == 16) {
                        result.InitGuid = new Guid(reader.ReadFixedBytes(16));
                        return result;
                    }
                }
                result.IsInvalid = true;
                return result;
            }

            public static void WriteRaw(Writer writer) {
                writer.WriteByte(1);
            }

            public static void WriteData(Writer writer, Guid? initGuid, uint packetId, bool requiresAck) {
                byte b = (byte)(requiresAck ? 3 : 2);
                if (initGuid.HasValue) {
                    b |= 0x80;
                }
                writer.WriteByte(b);
                if (initGuid.HasValue) {
                    writer.WriteFixedBytes(initGuid.Value.ToByteArray());
                }
                writer.WriteUInt32(packetId);
            }

            public static void WriteDataAck(Writer writer) {
                writer.WriteByte(4);
            }

            public static void WriteInitAck(Writer writer, Guid initGuid) {
                writer.WriteByte(5);
                writer.WriteFixedBytes(initGuid.ToByteArray());
            }
        }

        public struct MessagePartHeader {
            public bool IsInvalid;

            public uint MessageId;

            public uint? SequenceIndex;

            public int PartIndex;

            public bool IsFinalPart;

            public int DataSize;

            public static MessagePartHeader Read(Reader reader) {
                MessagePartHeader result = default;
                try {
                    byte b = reader.ReadByte();
                    result.MessageId = (b & 1) != 0 || (b & 4) == 0 ? reader.ReadUInt32() : 0u;
                    result.SequenceIndex = (b & 8) != 0 ? new uint?(reader.ReadUInt32()) : null;
                    result.PartIndex = (b & 1) != 0 ? reader.ReadPackedInt32() : 0;
                    result.DataSize = (b & 2) != 0 ? reader.ReadPackedInt32() : -1;
                    result.IsFinalPart = (b & 4) != 0;
                }
                catch (Exception) {
                    result.IsInvalid = true;
                }
                return result;
            }

            public static void Write(Writer writer, uint messageId, uint? sequenceIndex, int partIndex, bool isFinalPart, int dataSize) {
                byte b = 0;
                if (partIndex != 0) {
                    b |= 1;
                }
                if (dataSize >= 0) {
                    b |= 2;
                }
                if (isFinalPart) {
                    b |= 4;
                }
                if (sequenceIndex.HasValue) {
                    b |= 8;
                }
                writer.WriteByte(b);
                if (partIndex != 0
                    || !isFinalPart) {
                    writer.WriteUInt32(messageId);
                }
                if (sequenceIndex.HasValue) {
                    writer.WriteUInt32(sequenceIndex.Value);
                }
                if (partIndex != 0) {
                    writer.WritePackedInt32(partIndex);
                }
                if (dataSize >= 0) {
                    writer.WritePackedInt32(dataSize);
                }
            }
        }

        public class UnackedPacket {
            public double LastSendTime;

            public int SendCount;

            public Packet Packet;
        }

        public volatile bool IsDisposed;

        public Alarm Alarm;

        public uint NextPacketId;

        public uint NextMessageId;

        public Dictionary<IPEndPoint, Connection> Connections = new();

        public List<uint> ToRemoveUInt = new();

        public List<IPEndPoint> ToRemoveEndpoint = new();

        public static long StartTimestamp = Stopwatch.GetTimestamp();

        public CommSettings Settings { get; set; } = new();

        public ITransmitter Transmitter { get; set; }

        public IPEndPoint Address => Transmitter.Address;

        public object Lock { get; } = new();

        public event Action<Packet> Received;

        public event Action<Exception> Error;

        public event Action<string> Debug;

        public Comm(int localPort = 0) : this(new UdpTransmitter(localPort)) { }

        public Comm(ITransmitter transmitter) => Transmitter = transmitter ?? throw new ArgumentNullException("transmitter");

        public void Start() {
            lock (Lock) {
                CheckNotDisposed();
                if (Alarm != null) {
                    throw new InvalidOperationException("Comm is already started.");
                }
                Transmitter.Error += delegate(Exception e) { InvokeError(e); };
                Transmitter.PacketReceived += delegate(Packet packet) {
                    lock (Lock) {
                        if (!IsDisposed) {
                            ProcessReceivedPacket(packet);
                        }
                    }
                };
                Alarm = new Alarm(AlarmFunction);
                Alarm.Error += delegate(Exception e) { InvokeError(e); };
                Alarm.Set(0.0);
            }
        }

        public void Dispose() {
            lock (Lock) {
                if (IsDisposed) {
                    return;
                }
                IsDisposed = true;
            }
            Alarm?.Dispose();
            Transmitter?.Dispose();
        }

        public void Send(IPEndPoint address, DeliveryMode deliveryMode, byte[] bytes) {
            Send(address, deliveryMode, [bytes]);
        }

        public void Send(IPEndPoint address, DeliveryMode deliveryMode, IEnumerable<byte[]> bytes) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if ((Equals(address.Address, UdpTransmitter.IPV4BroadcastAddress) || Equals(address.Address, UdpTransmitter.IPV6BroadcastAddress))
                    && deliveryMode != 0) {
                    throw new InvalidOperationException("Broadcast messages must use DeliveryMode.Raw");
                }
                SendMessages(address, bytes.ToArray(), deliveryMode);
            }
        }

        public int GetUnackedPacketsCount(IPEndPoint address) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (!Connections.TryGetValue(address, out Connection value)) {
                    return 0;
                }
                return value.UnackedPackets.Count;
            }
        }

        public static double GetTime() => (Stopwatch.GetTimestamp() - StartTimestamp) / (double)Stopwatch.Frequency;

        public void AlarmFunction() {
            lock (Lock) {
                if (!IsDisposed) {
                    ProcessConnections();
                    float num = 0.25f;
                    Alarm.Set(num * Settings.ResendPeriods[0]);
                }
            }
        }

        public void ProcessReceivedPacket(Packet packet) {
            double time = GetTime();
            Reader reader = new(packet.Bytes);
            PacketHeader packetHeader = PacketHeader.Read(reader);
            if (packetHeader.IsInvalid) {
                InvokeError(new ProtocolViolationException($"Invalid packet header received from {packet.Address}, dropping packet"));
                return;
            }
            if (packetHeader.PacketType == PacketType.RawData) {
                int count = reader.Length - reader.Position;
                byte[] bytes = reader.ReadFixedBytes(count);
                InvokeReceived(packet.Address, bytes);
                return;
            }
            if (!Connections.TryGetValue(packet.Address, out Connection value)) {
                value = new Connection();
                Connections.Add(packet.Address, value);
            }
            value.LastReceiveTime = time;
            if (packetHeader.PacketType == PacketType.UnreliableData
                || packetHeader.PacketType == PacketType.ReliableData) {
                if (packetHeader.IsConnectionInit) {
                    if (value.TheirGuid == Guid.Empty
                        || packetHeader.InitGuid != value.TheirGuid) {
                        value.NewTheirGuid(packetHeader.InitGuid);
                    }
                    if (!value.InitAckConfirmed
                        && time - value.LastInitAckSendTime >= Settings.ResendPeriods[0]) {
                        SendInitAckPacket(packet.Address, packetHeader.InitGuid, value);
                    }
                }
                else {
                    value.InitAckConfirmed = true;
                }
                bool flag = packetHeader.PacketType == PacketType.ReliableData;
                if (flag) {
                    value.PacketIdsToAck.Add(packetHeader.PacketId);
                }
                if (value.ReceivedPacketIdsOld.Contains(packetHeader.PacketId)) {
                    value.ReceivedPacketIdsCurrent.Add(packetHeader.PacketId);
                }
                else {
                    if (value.ReceivedPacketIdsCurrent.Contains(packetHeader.PacketId)) {
                        return;
                    }
                    value.ReceivedPacketIdsCurrent.Add(packetHeader.PacketId);
                    while (reader.Position < reader.Length) {
                        MessagePartHeader messagePartHeader = MessagePartHeader.Read(reader);
                        if (messagePartHeader.IsInvalid) {
                            InvokeError(
                                new ProtocolViolationException(
                                    $"Invalid message part header received from {packet.Address}, dropping rest of the packet"
                                )
                            );
                            break;
                        }
                        int count2 = messagePartHeader.DataSize >= 0 ? messagePartHeader.DataSize : reader.Length - reader.Position;
                        byte[] array = reader.ReadFixedBytes(count2);
                        if (messagePartHeader.PartIndex == 0
                            && messagePartHeader.IsFinalPart) {
                            ProcessReceivedMessage(packet.Address, value, messagePartHeader, array, flag);
                            continue;
                        }
                        if (!value.MessageParts.TryGetValue(messagePartHeader.MessageId, out MessageParts value2)) {
                            value2 = new MessageParts();
                            value.MessageParts.Add(messagePartHeader.MessageId, value2);
                        }
                        value2.LastReceiveTime = time;
                        value2.Parts[messagePartHeader.PartIndex] = array;
                        if (messagePartHeader.IsFinalPart) {
                            value2.LastPartIndex = messagePartHeader.PartIndex;
                        }
                        if (value2.LastPartIndex < 0
                            || value2.Parts.Count != value2.LastPartIndex + 1) {
                            continue;
                        }
                        bool flag2 = true;
                        int num = 0;
                        for (int i = 0; i <= value2.LastPartIndex; i++) {
                            if (!value2.Parts.TryGetValue(i, out byte[] value3)) {
                                flag2 = false;
                                break;
                            }
                            num += value3.Length;
                        }
                        if (flag2) {
                            byte[] array2 = new byte[num];
                            int j = 0;
                            int num2 = 0;
                            for (; j <= value2.LastPartIndex; j++) {
                                byte[] array3 = value2.Parts[j];
                                Array.Copy(array3, 0, array2, num2, array3.Length);
                                num2 += array3.Length;
                            }
                            ProcessReceivedMessage(packet.Address, value, messagePartHeader, array2, flag);
                        }
                        value.MessageParts.Remove(messagePartHeader.MessageId);
                    }
                }
            }
            else if (packetHeader.PacketType == PacketType.DataAck) {
                while (reader.Length - reader.Position >= 4) {
                    uint key = reader.ReadUInt32();
                    value.UnackedPackets.Remove(key);
                }
            }
            else if (packetHeader.PacketType == PacketType.InitAck) {
                if (packetHeader.InitGuid == value.OurGuid) {
                    value.InitAckReceived = true;
                }
                else {
                    InvokeError(
                        new ProtocolViolationException(
                            $"Invalid InitAck Guid received from {packet.Address} (received {packetHeader.InitGuid.ToString()}, expected {value.OurGuid.ToString()}), ignoring"
                        )
                    );
                }
            }
            else {
                InvokeError(
                    new ProtocolViolationException($"Invalid packet type {(int)packetHeader.PacketType} received from {packet.Address}, ignoring")
                );
            }
        }

        public void ProcessReceivedMessage(IPEndPoint address,
            Connection connection,
            MessagePartHeader messagePartHeader,
            byte[] bytes,
            bool isReliable) {
            if (messagePartHeader.SequenceIndex.HasValue) {
                if (isReliable) {
                    if (!connection.NextReliableReceiveSequenceIndex.HasValue
                        || messagePartHeader.SequenceIndex.Value == connection.NextReliableReceiveSequenceIndex) {
                        connection.NextReliableReceiveSequenceIndex = messagePartHeader.SequenceIndex.Value + 1;
                        InvokeReceived(address, bytes);
                        byte[] value;
                        while (connection.SequencedBytes.TryGetValue(connection.NextReliableReceiveSequenceIndex.Value, out value)) {
                            connection.SequencedBytes.Remove(connection.NextReliableReceiveSequenceIndex.Value);
                            connection.NextReliableReceiveSequenceIndex++;
                            InvokeReceived(address, value);
                        }
                    }
                    else {
                        connection.SequencedBytes.Add(messagePartHeader.SequenceIndex.Value, bytes);
                    }
                }
                else if (!connection.NextUnreliableReceiveSequenceIndex.HasValue
                    || CompareSequenceNumbers(messagePartHeader.SequenceIndex.Value, connection.NextUnreliableReceiveSequenceIndex.Value) >= 0) {
                    connection.NextUnreliableReceiveSequenceIndex = messagePartHeader.SequenceIndex.Value + 1;
                    InvokeReceived(address, bytes);
                }
            }
            else {
                InvokeReceived(address, bytes);
            }
        }

        public void ProcessConnections() {
            double time = GetTime();
            foreach (KeyValuePair<IPEndPoint, Connection> connection in Connections) {
                IPEndPoint key = connection.Key;
                Connection value = connection.Value;
                if (value.PacketIdsToAck.Count > 0) {
                    SendDataAckPackets(key, value.PacketIdsToAck, value);
                    value.PacketIdsToAck.Clear();
                }
                foreach (KeyValuePair<uint, UnackedPacket> unackedPacket in value.UnackedPackets) {
                    float num = Settings.ResendPeriods[Math.Min(unackedPacket.Value.SendCount - 1, Settings.ResendPeriods.Length - 1)];
                    if (unackedPacket.Value.SendCount - 1 >= Settings.MaxResends) {
                        ToRemoveUInt.Add(unackedPacket.Key);
                    }
                    else if (time >= unackedPacket.Value.LastSendTime + num) {
                        SendPacket(unackedPacket.Value.Packet, value);
                        unackedPacket.Value.LastSendTime = time;
                        unackedPacket.Value.SendCount++;
                    }
                }
                foreach (uint item in ToRemoveUInt) {
                    value.UnackedPackets.Remove(item);
                }
                ToRemoveUInt.Clear();
                foreach (KeyValuePair<uint, MessageParts> messagePart in value.MessageParts) {
                    if (time - messagePart.Value.LastReceiveTime > Settings.MaxResends * Settings.ResendPeriods[Settings.ResendPeriods.Length - 1]) {
                        ToRemoveUInt.Add(messagePart.Key);
                    }
                }
                foreach (uint item2 in ToRemoveUInt) {
                    value.MessageParts.Remove(item2);
                }
                ToRemoveUInt.Clear();
                if (time >= value.LastReceivedPacketsIdsSwitchTime + Settings.DuplicatePacketsDetectionTime) {
                    value.ReceivedPacketIdsOld.Clear();
                    HashSet<uint> receivedPacketIdsOld = value.ReceivedPacketIdsOld;
                    value.ReceivedPacketIdsOld = value.ReceivedPacketIdsCurrent;
                    value.ReceivedPacketIdsCurrent = receivedPacketIdsOld;
                    value.LastReceivedPacketsIdsSwitchTime = time;
                }
                if (time - value.LastSendTime >= Settings.IdleTime
                    && time - value.LastReceiveTime >= Settings.IdleTime) {
                    ToRemoveEndpoint.Add(key);
                }
            }
            foreach (IPEndPoint item3 in ToRemoveEndpoint) {
                Connections.Remove(item3);
            }
            ToRemoveEndpoint.Clear();
        }

        public void SendMessages(IPEndPoint address, byte[][] bytes, DeliveryMode deliveryMode) {
            if (deliveryMode == DeliveryMode.Raw) {
                foreach (byte[] bytes2 in bytes) {
                    Writer writer = new();
                    PacketHeader.WriteRaw(writer);
                    writer.WriteFixedBytes(bytes2);
                    Transmitter.SendPacket(new Packet(address, writer.GetBytes()));
                }
                return;
            }
            if (!Connections.TryGetValue(address, out Connection value)) {
                value = new Connection();
                Connections.Add(address, value);
            }
            Writer writer2 = null;
            byte[] array = null;
            int num = 0;
            uint packetId = 0u;
            uint messageId = 0u;
            uint? sequenceIndex = 0u;
            int num2 = 0;
            int num3 = 0;
            while (true) {
                if (array == null) {
                    if (num3 >= bytes.Length) {
                        break;
                    }
                    array = bytes[num3++];
                    num = 0;
                    messageId = NextMessageId++;
                    sequenceIndex = deliveryMode switch {
                        DeliveryMode.UnreliableSequenced => value.NextUnreliableSendSequenceIndex++,
                        DeliveryMode.ReliableSequenced => value.NextReliableSendSequenceIndex++,
                        _ => null
                    };
                    num2 = 0;
                }
                if (writer2 == null) {
                    writer2 = new Writer();
                    packetId = NextPacketId;
                    NextPacketId++;
                    Guid? initGuid = value.InitAckReceived ? null : new Guid?(value.OurGuid);
                    PacketHeader.WriteData(
                        writer2,
                        initGuid,
                        packetId,
                        deliveryMode == DeliveryMode.Reliable || deliveryMode == DeliveryMode.ReliableSequenced
                    );
                }
                int position = writer2.Position;
                int num4 = array.Length - num;
                MessagePartHeader.Write(writer2, messageId, sequenceIndex, num2, true, num4);
                if (writer2.Position + num4 <= Transmitter.MaxPacketSize) {
                    writer2.WriteFixedBytes(array, num, num4);
                    array = null;
                    continue;
                }
                writer2.Length = position;
                MessagePartHeader.Write(writer2, messageId, sequenceIndex, num2, true, -1);
                if (writer2.Position + num4 <= Transmitter.MaxPacketSize) {
                    writer2.WriteFixedBytes(array, num, num4);
                    array = null;
                    continue;
                }
                writer2.Length = position;
                MessagePartHeader.Write(writer2, messageId, sequenceIndex, num2, false, -1);
                num4 = Transmitter.MaxPacketSize - writer2.Position;
                if (num4 > 0) {
                    writer2.WriteFixedBytes(array, num, num4);
                    num += num4;
                    num2++;
                }
                else {
                    writer2.Length = position;
                }
                SendDataPacket(address, writer2.GetBytes(), packetId, deliveryMode, value);
                writer2 = null;
            }
            if (writer2 != null
                && writer2.Position > 0) {
                SendDataPacket(address, writer2.GetBytes(), packetId, deliveryMode, value);
            }
        }

        public void SendDataPacket(IPEndPoint address, byte[] bytes, uint packetId, DeliveryMode deliveryMode, Connection connection) {
            Packet packet = new(address, bytes);
            if (deliveryMode == DeliveryMode.Reliable
                || deliveryMode == DeliveryMode.ReliableSequenced) {
                connection.UnackedPackets.Add(packetId, new UnackedPacket { Packet = packet, SendCount = 1, LastSendTime = GetTime() });
            }
            SendPacket(packet, connection);
        }

        public void SendDataAckPackets(IPEndPoint address, List<uint> acks, Connection connection) {
            int num = 0;
            while (num < acks.Count) {
                Writer writer = new();
                PacketHeader.WriteDataAck(writer);
                int num2 = Transmitter.MaxPacketSize - writer.Position;
                int num3 = Math.Min(val2: acks.Count - num, val1: num2 / 4);
                for (int i = 0; i < num3; i++) {
                    writer.WriteUInt32(acks[num++]);
                }
                SendPacket(new Packet(address, writer.GetBytes()), connection);
            }
        }

        public void SendInitAckPacket(IPEndPoint address, Guid initGuid, Connection connection) {
            connection.LastInitAckSendTime = GetTime();
            Writer writer = new();
            PacketHeader.WriteInitAck(writer, initGuid);
            SendPacket(new Packet(address, writer.GetBytes()), connection);
        }

        public void SendPacket(Packet packet, Connection connection) {
            connection.LastSendTime = GetTime();
            Transmitter.SendPacket(packet);
        }

        public void CheckNotDisposed() {
            if (IsDisposed) {
                throw new ObjectDisposedException("Comm");
            }
        }

        public void CheckNotDisposedAndStarted() {
            CheckNotDisposed();
            if (Alarm == null) {
                throw new InvalidOperationException("Comm is not started.");
            }
        }

        public void InvokeReceived(IPEndPoint address, byte[] bytes) {
            try {
                Received?.Invoke(new Packet(address, bytes));
            }
            catch (Exception error) {
                InvokeError(error);
            }
        }

        public void InvokeError(Exception error) {
            Error?.Invoke(error);
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string format, params object[] args) {
            if (Debug != null) {
                Debug?.Invoke(string.Format(format, args));
            }
        }

        public static int CompareSequenceNumbers(uint s1, uint s2) {
            if (s1 == s2) {
                return 0;
            }
            if ((s1 < s2 && s2 - s1 < int.MaxValue)
                || (s1 > s2 && s1 - s2 > int.MaxValue)) {
                return -1;
            }
            return 1;
        }
    }
}